﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.Web.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

namespace MvcApplicationBindersSample.Binders {
    public class ImmutableObjectModelBinder : IExtensibleModelBinder {

        public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext) {
            // Provider ensures that there's exactly one constructor and that its parameters
            // have corresponding property metadatas.
            IEnumerable<ModelMetadata> propertyMetadatas = bindingContext.ModelType.GetConstructors()[0].GetParameters().Select(p => bindingContext.PropertyMetadata[p.Name]);
            ComplexModelDto dto = CreateAndPopulateDto(controllerContext, bindingContext, propertyMetadatas);
            ProcessDto(controllerContext, bindingContext, dto);

            return true; // tried to set a model, and validation should run
        }

        private ComplexModelDto CreateAndPopulateDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas) {
            ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);

            // The binding context's copy constructor clones many useful properties such as the value provider,
            // provider collection, ModelState, and others. We only set the properties that must be changed.
            ExtensibleModelBindingContext dtoBindingContext = new ExtensibleModelBindingContext(bindingContext) {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)),
                ModelName = bindingContext.ModelName
            };

            // Get the binder that can handle our DTO, then call into it to retrieve values for this model's
            // properties. The inner binder won't set the properties directly, but it will populate the DTO
            // with the appropriate values so that we can read them later.
            IExtensibleModelBinder dtoBinder = bindingContext.ModelBinderProviders.GetBinder(controllerContext, dtoBindingContext);
            dtoBinder.BindModel(controllerContext, dtoBindingContext);
            return (ComplexModelDto)dtoBindingContext.Model;
        }

        private static object GetPropertyDefaultValue(ModelMetadata propertyMetadata) {
            // returns the [DefaultValue] value applied to the property, if any
            PropertyDescriptor pDesc = new AssociatedMetadataTypeTypeDescriptionProvider(propertyMetadata.ContainerType).GetTypeDescriptor(propertyMetadata.ContainerType).GetProperties().Find(propertyMetadata.PropertyName, true /* ignoreCase */);
            DefaultValueAttribute attr = pDesc.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
            return (attr != null) ? attr.Value : null;
        }

        private static object GetTypeDefaultValue(Type type) {
            // Non-nullable value types guaranteed to have a public parameterless constructor,
            // which corresponds to default(T).
            return (TypeAllowsNullValue(type)) ? null : Activator.CreateInstance(type);
        }

        private void ProcessDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ComplexModelDto dto) {
            List<object> ctorParameterValues = new List<object>();

            // need to retrieve the value for each property from the DTO
            foreach (ModelMetadata propertyMetadata in dto.PropertyMetadata) {
                ComplexModelDtoResult dtoResult;
                dto.Results.TryGetValue(propertyMetadata, out dtoResult);

                object currentValue = null;
                if (dtoResult != null) {
                    currentValue = dtoResult.Model;
                    // need to merge each of the child validation nodes up so that the runner
                    // can execute all of them
                    bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
                }

                // If there was no incoming value for this property, use its [DefaultValue] value,
                // otherwise use default(T) as a last resort.
                currentValue = currentValue ?? GetPropertyDefaultValue(propertyMetadata) ?? GetTypeDefaultValue(propertyMetadata.ModelType);
                ctorParameterValues.Add(currentValue);
            }

            // finally, instantiate the model
            object newModel = null;
            try {
                newModel = Activator.CreateInstance(bindingContext.ModelType, ctorParameterValues.ToArray());
            }
            catch (Exception ex) {
                // record error in ModelState
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
            }

            bindingContext.Model = newModel;

            // validate all properties, even those we didn't get values for
            bindingContext.ValidationNode.ValidateAllProperties = true;
        }

        private static bool TypeAllowsNullValue(Type type) {
            return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null);
        }

    }
}
